import docx
from docx.document import Document
from docx.oxml.table import CT_Tbl
from docx.oxml.text.paragraph import CT_P
from docx.table import _Cell, Table
from docx.text.paragraph import Paragraph, Run
from pathlib import Path
import comtypes.client
from typing import Union, List, Dict, Optional
from loguru import logger
from collections import Counter
import tempfile
import html2text
import re


def doc2docx(doc_file: Union[Path, str], docx_file: Union[Path, str]):
    """
    Doc文件转Docx
    """
    if not Path(doc_file).exists():
        logger.error(f'{doc_file} 文件不存在. ')
        return
    if Path(doc_file).suffix.lower() != '.doc':
        logger.error(f'待转换文件 {doc_file} 不是Doc. ')
        return
    if Path(docx_file).suffix.lower() != '.docx':
        logger.error(f'待保存文件 {docx_file} 不是Docx. ')
        return

    word = comtypes.client.CreateObject('Word.Application')
    word.Visible = False
    src_file = doc_file.as_posix() if isinstance(doc_file, Path) else doc_file
    des_file = docx_file.as_posix() if isinstance(docx_file, Path) else docx_file
    doc = None
    try:
        doc = word.Documents.Open(src_file, ReadOnly=True)
        doc.SaveAs(des_file, FileFormat=16)
    except Exception as e:
        logger.error(e)
        logger.error(f'{doc_file} 转换失败. ')
    finally:
        if doc is not None:
            doc.Close()
    word.Quit()


class DocParser(object):

    @staticmethod
    def format_run(run: Run, ignore_unimportant: bool = True):
        """
        :param run: 段落中的 run
        :param ignore_unimportant: 是否忽略次重要的样式，包括：斜体，下划线，字体名称，字体大小
        :return:
        """
        # 加粗
        if run.font.bold is not None:
            run.text = '<b>' + run.text + '</b>'

        # 斜体
        if not ignore_unimportant:
            if run.font.italic is not None:
                run.text = '<i>' + run.text + '</i>'

        # 下划线
        if not ignore_unimportant:
            if run.font.underline is not None:
                run.text = '<u>' + run.text + '</u>'

        # 删除线
        if run.font.strike is not None:
            run.text = '<s>' + run.text + '</s>'

        # 文字颜色
        if run.font.color.rgb is not None:
            if '</font>' not in run.text:
                run.text = run.text + '</font>'
            run.text = '<font color=#{}>'.format(run.font.color.rgb) + run.text

        # 字体高亮
        if run.font.highlight_color is not None:
            # 这里我尝试过了下面的语句，根据https://blog.csdn.net/ningmengshuxiawo/article/details/109112540介绍是可以更换成红色的
            # <mark style="background:red" >这里是输入的文本</mark>
            # 但是我自己尝试的时候发现背景色并不能更换，这里就直接用黄色高亮标记
            run.text = '<mark>' + run.text + '</mark>'

        # 设置字体，默认为宋体
        if not ignore_unimportant:
            if '</font>' not in run.text:
                run.text = run.text + '</font>'
            if run.font.name is None:
                if re.search('[\u4E00-\u9F5A]', run.text) is None:
                    run.text = '<font face="Times New Roman">' + run.text
                else:
                    run.text = '<font face="宋体">' + run.text
            else:
                run.text = '<font face={}>'.format(run.font.name) + run.text

        # 文字大小
        if not ignore_unimportant:
            if run.font.size is not None:
                font_size = run.font.size.pt // 4
                if '</font>' not in run.text:
                    run.text = run.text + '</font>'
                run.text = '<font size={}>'.format(font_size) + run.text


    @staticmethod
    def format_paragraph(para: Paragraph, ignore_unimportant: bool = True):
        # 读取段落标题 docx中最高支持9级标题，但Markdown最高只支持６级标题
        # paragraph.style.name 返回值"Heading 标题等级数字"

        if para.style.name is not None and 'Heading' in para.style.name:  # 判断段落是否为标题
            try:
                level = eval(para.style.name[-1])
            except Exception as e:
                logger.error(e)
                level = 0
            for run in para.runs:
                if run.text.find('size=') != -1:
                    i = run.text.index('size=') + 5  # 查询标题中设置标题字号的文本位置
                    run.text = run.text[:i] + '{}'.format(7 - level) + run.text[i + 1:]  # 1级标题文字大小为6
            para.text = '#' * level + ' ' + para.text

        # 首行缩进, 单位支持Pt、Cm、Mm、Inches等，如果想要缩进几个字符，需要自己进行转换，因为不同字号字符占用的磅数是不同的（五号字体 = 10.5pt = 3.70mm = 14px = 0.146inch）
        if not ignore_unimportant:
            if para.paragraph_format.first_line_indent is not None:  # 判断段落是否使用首行缩进
                para.text = '&#8195;' * 2 + para.text


    def read_paragraph(self,  para: Paragraph,
                       is_code: bool = False,
                       format_text: bool = True,
                       ignore_unimportant: bool = True) -> Optional[Dict]:
        if para.text == '':
            return None
        font_name = self.get_almost([run.font.name for run in para.runs])
        font_size = self.get_almost([getattr(run.font.size, 'pt', None) for run in para.runs])
        font_bold = self.get_first([run.font.bold for run in para.runs])

        # 如果不属于代码块，则修改文字格式及段落格式，否则不做修改
        if format_text and not is_code:
            for run in para.runs:
                try:
                    self.format_run(run, ignore_unimportant)
                except Exception as e:
                    logger.error(e)
            self.format_paragraph(para, ignore_unimportant)

        text_block = {
            "blockType": "text",
            "content": para.text,
            "textFont": {
                "fontName": font_name,
                "fontSize": font_size,
                "bold": font_bold
            }
        }
        return text_block


    @staticmethod
    def read_table(table):
        """ 读取表格 """
        table_html = '<table>\n'

        row_cells, col_cells = [], []
        for row in table.rows:
            try:
                row_cells.append([cell for cell in row.cells])
            except Exception as e:
                logger.error(e)
                row_cells.append([])
        for col in table.columns:
            try:
                col_cells.append([cell for cell in col.cells])
            except Exception as e:
                logger.error(e)
                col_cells.append([])

        row_temp, col_temp = [], []
        for i in range(len(table.rows)):
            table_html = table_html + '<tr>'
            for j in range(len(table.columns)):
                if i >= len(col_cells[j]):
                    continue
                if j >= len(row_cells[i]):
                    continue
                col_counts = row_cells[i].count(row_cells[i][j])  # 确定行中重复地址数目以确定合并数量
                row_counts = col_cells[j].count(col_cells[j][i])  # 确定列中重复地址数目以确定合并数量
                if row_cells[i][j] not in row_temp and col_cells[j][i] not in col_temp:  # 行列地址值去重
                    if col_counts == 1 and row_counts == 1:  # 单元格没有合并
                        table_html = table_html + '<td>' + table.rows[i].cells[j].text + '</td>'
                    elif col_counts != 1 and row_counts == 1:  # 横向合并
                        table_html = table_html + '<td colspan={}>'. \
                            format(col_counts) + table.rows[i].cells[j].text + '</td>'
                    elif col_counts == 1 and row_counts != 1:  # 纵向合并
                        table_html = table_html + '<td rowspan={}>'. \
                            format(row_counts) + table.rows[i].cells[j].text + '</td>'
                    else:  # 横纵同时合并
                        table_html = table_html + '<td colspan={0} rowspan={1}>'. \
                            format(col_counts, row_counts) + table.rows[i].cells[j].text + '</td>'
                row_temp.append(row_cells[i][j])
                col_temp.append(col_cells[j][i])
            table_html = table_html + '</tr>\n'

        table_html = table_html + '</table>'
        table_markdown = html2text.html2text(table_html)

        table_block = {
            "blockType": "table",
            "content": table_html,
            "words": html2text.html2text(table_markdown),
        }
        return table_block


    @staticmethod
    def iter_block_items(parent):
        """
        按文档顺序读取文本块及表格
        """
        if isinstance(parent, Document):
            parent_elm = parent.element.body
        elif isinstance(parent, _Cell):
            parent_elm = parent._tc
        else:
            raise ValueError("something's not right")

        for child in parent_elm.iterchildren():
            if isinstance(child, CT_P):
                yield Paragraph(child, parent)
            elif isinstance(child, CT_Tbl):
                yield Table(child, parent)


    def parse_docx(self, docx_file: Union[Path, str],
                   format_text: bool = True,
                   ignore_unimportant: bool = True) -> List[Dict]:
        """
        :param docx_file: Docx文档.
        :param format_text: 是否输出原文字体的样式.
            例如，合同中某些段落文字上可能有删除线，表示作废的条款，如果仅输出文本，则可能与原文不符.
        :param ignore_unimportant: 是否忽略次重要的样式，包括：斜体，下划线，字体名称，字体大小
        :return: 段落列表.
        """

        doc = docx.Document(docx_file)
        text_blocks = []
        block_index = 0
        is_code = False
        for block in self.iter_block_items(doc):
            if isinstance(block, Paragraph):
                # 代码块开始或结束
                if re.search('^\s*```', block.text) is not None:
                    is_code = True if not is_code else False
                text_block = self.read_paragraph(block, is_code, format_text, ignore_unimportant)
            elif isinstance(block, Table):
                text_block = self.read_table(block)
            else:
                raise NotImplementedError
            if text_block is not None:
                text_block['blockIndex'] = block_index
                text_blocks.append(text_block)
                block_index += 1
        return text_blocks


    def parse(self,file: Union[Path, str], format_text: bool = True, ignore_unimportant: bool = True) -> List[Dict]:
        if Path(file).suffix.lower() == '.docx':
            return self.parse_docx(file, format_text, ignore_unimportant)
        elif Path(file).suffix.lower() == '.doc':
            tmp_file = tempfile.NamedTemporaryFile(suffix='.docx', delete=False)
            tmp_file.close()
            docx_file = Path(tmp_file.name)
            doc2docx(file, docx_file)
            text_blocks = self.parse_docx(docx_file, format_text, ignore_unimportant)
            try:
                docx_file.unlink()
            except Exception as e:
                logger.error(e)
                logger.error(f'{docx_file} 删除失败. ')
            return text_blocks
        else:
            raise NotImplementedError


    @staticmethod
    def get_first(items: List):
        if len(items) == 0:
            return None
        return items[0]


    @staticmethod
    def get_almost(items: List):
        if len(items) == 0:
            return None
        counts = Counter(items)
        counts = sorted(counts.items(), key=lambda x: -x[1])
        return counts[0][0]

def parser_templates():
    """解析所有的合同模板"""
    import json

    parser = DocParser()
    file_dir = Path(r'D:\myworks\合同审查\合同模板\合同模板')
    json_dir = Path(r'D:\myworks\合同审查\合同模板\合同模板json(无格式)')
    mark_dir = Path(r'D:\myworks\合同审查\合同模板\合同模板markdown(无格式)')
    for idx, file in enumerate(file_dir.rglob('*.docx')):
        logger.info(f'{idx} ---  {file}')
        if file.name.startswith('~'):
            continue
        text_blocks = parser.parse(file, format_text=False, ignore_unimportant=True)
        json_file = json_dir / file.parent.name / f'{file.stem}.json'
        json_file.parent.mkdir(parents=True, exist_ok=True)
        with open(json_file, 'w', encoding='utf-8', newline='') as fw:
            json.dump(text_blocks, fw, ensure_ascii=False, indent=4)

        markdown = '\n'.join([item['content'] for item in text_blocks])
        mark_file = mark_dir / file.parent.name / f'{file.stem}.json'
        mark_file.parent.mkdir(parents=True, exist_ok=True)
        with open(mark_file, 'w', encoding='utf-8', newline='') as fw:
            fw.write(markdown)


if __name__  == '__main__':
    parser_templates()

    # parser = DocParser()
    # file = r'D:\myworks\合同审查\合同模板\合同模板\分销合作协议\代理协议范本(1).docx'
    # text_blocks = parser.parse(file)
    # markdown = '\n'.join([item['content'] for item in text_blocks])
    # print(markdown)
